线程同步之volatile

volatile是一个Java语言的关键字,在线程同步上有一席之地。相较于其他线程同步方法,volatile的使用简单,性能较好,同时也有其局限性。

内存模型

  • 每个线程在处理器的高速缓存中拥有一份工作内存,保存它用到的共享变量的副本。
  • 工作内存中的变量从主内存中读取拷贝,修改后写回主内存。

并发条件

并发编程时需满足三个条件才能正常工作。

可见性

  • 一个线程修改了变量,其他线程立即要能看到。
  • 通常工作内存中的变量修改后,不会立即写回主内存,从而产生脏读。

原子性

  • Do it all or don’t do it at all。
  • 只有最基本的赋值是原子操作,变量互相赋值、自增减均不是原子操作。

有序性

指令重排序:

  • 处理器为提高效率对代码的执行顺序优化,但保证执行结果和顺序执行的结果相同。
1
2
3
4
5
int i = 0;
int f = false;
i = 1; // 1
f = true; // 2
// 1和2无依赖,执行顺序可能为:12或21
1
2
3
4
5
int a = 10; // 1
int r = 2; // 2
a = a + 3; // 3
r = a*a; // 4
// 由于有依赖,可能的顺序1234, 1324, 2134
  • 多线程时,当依赖某个变量来判断语句的执行时,可能会因为指令重排序出错。
1
2
3
4
5
6
7
// T1
context = loadContext(); // 1
boolean inited = true; // 2
// T2
while(!inited) sleep(); // 1
postInit(context); // 2
// 出错:T1.2->T2.1->T2.2->T1.1
  • JVM有序性原则:happens-before(共8条)
    1. 单线程内执行结果“看起来”顺序(仍可能重排序)。
    2. 对同一个锁,unlock先于lock。
    3. 一个线程写一个变量,然后一个线程读该变量,则写先于读。
    4. A先于B,B先于C,则A先于C。

Volatile与并发

可见性

volatile可保证可见性:对变量的修改立即写回主内存;读取变量值时会从主内存读取。

原子性

volatile无法保证原子性:当面对非原子操作时,单句代码会被拆解执行。

1
2
3
4
5
volatile int inc = 10;
for(int i=0;i<1000;i++) {
inc++;
}
// 10个线程均执行以上循环,结果可能小于1000*10

inc++包含三步:从主内存读inc,计算inc’=inc+1,写主内存inc=inc’,以下情况会出错:

  1. T1从主内存读inc=10,写入T1工作内存。
  2. T2从主内存读inc=10,写入T2工作内存。
  3. T1计算inc’=11并写工作内存及主内存inc=11。
  4. T2计算inc’=11并写工作内存及主内存inc=11。

有序性

volatile可保证有序性:禁止指令重排序。
原则:

  1. 执行对volatile的读/写操作前,前面的更改一定全部执行,且结果对后面可见;后面的更改一定没有执行。
  2. 指令优化时,不能把对volatile的读/写操作放在其后,也不能把volatile读/写后面的操作放在其前。
1
2
3
4
5
6
7
8
9
10
11
//x、y为非volatile变量
//flag为volatile变量
int x, y;
volatile boolean f;
x = 2; // 1
y = 0; // 2
f = true; // 3
x = 4; // 4
y = -1; // 5
// 不可能的顺序:1或2在3后面;4或5在3前面
// 可能的顺序:12345, 12354, 21345, 21354

实际例子中保证有序性:

1
2
3
4
5
6
7
8
volatile boolean inited;
// T1
context = loadContext(); // 1
inited = true; // 2
// T2
while(!inited) sleep(); // 1
postInit(context); // 2
// T1.1必在T1.2前执行,因此避免出错

使用场景

  1. 状态标记
    利用有序性及可见性,需规避非原子性操作。
  2. 单例模式double check。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Singleton {
private volatile static Singleton instance = null;
private int x;
private Singleton() {x = 1;}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
/* 由于instance = new Singleton()非原子操作,会被拆成三步,利用volatile避免指令重排序避免出错
为对象分配内存 // 1
调用构造函数初始化成员变量(如有) // 2
将instance指向新对象 // 3
可能的顺序:123, 132,若instance为volatile则只能为123
出错的情况(instance不为volatile):
1. T1调用,instance = null, 通过判断拿到锁,开始初始化对象
2. 指令重排序为132
a. 为对象分配内存
b. 将instance指向新对象, instance != null
c. T1阻塞
d. 调用instance构造方法,初始化成员变量等
3. T2在2.b与2.d之间调用,由于instance != null,直接返回instance,指向已经分配内存但未调用构造方法的对象,此时成员变量未初始化,可能出错